/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.ejb.access;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;

import javax.ejb.EJBHome;
import javax.ejb.EJBObject;
import javax.naming.NamingException;

import org.aopalliance.intercept.MethodInvocation;

import org.springframework.lang.Nullable;
import org.springframework.remoting.RemoteConnectFailureException;
import org.springframework.remoting.RemoteLookupFailureException;
import org.springframework.remoting.rmi.RmiClientInterceptorUtils;

/**
 * Base class for interceptors proxying remote Stateless Session Beans.
 * Designed for EJB 2.x, but works for EJB 3 Session Beans as well.
 *
 * <p>Such an interceptor must be the last interceptor in the advice chain.
 * In this case, there is no target object.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 */
public abstract class AbstractRemoteSlsbInvokerInterceptor extends AbstractSlsbInvokerInterceptor {

    private boolean refreshHomeOnConnectFailure = false;

    private volatile boolean homeAsComponent = false;


    /**
     * Set whether to refresh the EJB home on connect failure.
     * Default is "false".
     * <p>Can be turned on to allow for hot restart of the EJB server.
     * If a cached EJB home throws an RMI exception that indicates a
     * remote connect failure, a fresh home will be fetched and the
     * invocation will be retried.
     *
     * @see java.rmi.ConnectException
     * @see java.rmi.ConnectIOException
     * @see java.rmi.NoSuchObjectException
     */
    public void setRefreshHomeOnConnectFailure(boolean refreshHomeOnConnectFailure) {
        this.refreshHomeOnConnectFailure = refreshHomeOnConnectFailure;
    }

    @Override
    protected boolean isHomeRefreshable() {
        return this.refreshHomeOnConnectFailure;
    }


    /**
     * Check for EJB3-style home object that serves as EJB component directly.
     */
    @Override
    protected Method getCreateMethod(Object home) throws EjbAccessException {
        if (this.homeAsComponent) {
            return null;
        }
        if (!(home instanceof EJBHome)) {
            // An EJB3 Session Bean...
            this.homeAsComponent = true;
            return null;
        }
        return super.getCreateMethod(home);
    }


    /**
     * Fetches an EJB home object and delegates to {@code doInvoke}.
     * <p>If configured to refresh on connect failure, it will call
     * {@link #refreshAndRetry} on corresponding RMI exceptions.
     *
     * @see #getHome
     * @see #doInvoke
     * @see #refreshAndRetry
     */
    @Override
    @Nullable
    public Object invokeInContext(MethodInvocation invocation) throws Throwable {
        try {
            return doInvoke(invocation);
        } catch (RemoteConnectFailureException ex) {
            return handleRemoteConnectFailure(invocation, ex);
        } catch (RemoteException ex) {
            if (isConnectFailure(ex)) {
                return handleRemoteConnectFailure(invocation, ex);
            } else {
                throw ex;
            }
        }
    }

    /**
     * Determine whether the given RMI exception indicates a connect failure.
     * <p>The default implementation delegates to RmiClientInterceptorUtils.
     *
     * @param ex the RMI exception to check
     * @return whether the exception should be treated as connect failure
     * @see org.springframework.remoting.rmi.RmiClientInterceptorUtils#isConnectFailure
     */
    protected boolean isConnectFailure(RemoteException ex) {
        return RmiClientInterceptorUtils.isConnectFailure(ex);
    }

    @Nullable
    private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable {
        if (this.refreshHomeOnConnectFailure) {
            if (logger.isDebugEnabled()) {
                logger.debug("Could not connect to remote EJB [" + getJndiName() + "] - retrying", ex);
            } else if (logger.isWarnEnabled()) {
                logger.warn("Could not connect to remote EJB [" + getJndiName() + "] - retrying");
            }
            return refreshAndRetry(invocation);
        } else {
            throw ex;
        }
    }

    /**
     * Refresh the EJB home object and retry the given invocation.
     * Called by invoke on connect failure.
     *
     * @param invocation the AOP method invocation
     * @return the invocation result, if any
     * @throws Throwable in case of invocation failure
     * @see #invoke
     */
    @Nullable
    protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable {
        try {
            refreshHome();
        } catch (NamingException ex) {
            throw new RemoteLookupFailureException("Failed to locate remote EJB [" + getJndiName() + "]", ex);
        }
        return doInvoke(invocation);
    }


    /**
     * Perform the given invocation on the current EJB home.
     * Template method to be implemented by subclasses.
     *
     * @param invocation the AOP method invocation
     * @return the invocation result, if any
     * @throws Throwable in case of invocation failure
     * @see #getHome
     * @see #newSessionBeanInstance
     */
    @Nullable
    protected abstract Object doInvoke(MethodInvocation invocation) throws Throwable;


    /**
     * Return a new instance of the stateless session bean.
     * To be invoked by concrete remote SLSB invoker subclasses.
     * <p>Can be overridden to change the algorithm.
     *
     * @throws NamingException if thrown by JNDI
     * @throws InvocationTargetException if thrown by the create method
     * @see #create
     */
    protected Object newSessionBeanInstance() throws NamingException, InvocationTargetException {
        if (logger.isDebugEnabled()) {
            logger.debug("Trying to create reference to remote EJB");
        }
        Object ejbInstance = create();
        if (logger.isDebugEnabled()) {
            logger.debug("Obtained reference to remote EJB: " + ejbInstance);
        }
        return ejbInstance;
    }

    /**
     * Remove the given EJB instance.
     * To be invoked by concrete remote SLSB invoker subclasses.
     *
     * @param ejb the EJB instance to remove
     * @see javax.ejb.EJBObject#remove
     */
    protected void removeSessionBeanInstance(@Nullable EJBObject ejb) {
        if (ejb != null && !this.homeAsComponent) {
            try {
                ejb.remove();
            } catch (Throwable ex) {
                logger.warn("Could not invoke 'remove' on remote EJB proxy", ex);
            }
        }
    }

}
